在使用者空間呼叫 malloc() 成功時,我們通常會說「配置到記憶體了」。但這句話很容易讓人誤以為 kernel 已經立刻找好實體 page,並把 RAM 交給行程。實際上,malloc()、虛擬位址空間與實體記憶體配置是三個不同層次;它們常因延遲配置與 page fault 而在不同時間發生。
今天從一次普通的 malloc() 出發,拆解 allocator、系統呼叫與 demand paging 之間的分工。
malloc() 是函式庫介面malloc() 不是 Linux system call,而是 C 函式庫提供的使用者空間配置介面。以 glibc 為例,allocator 會管理行程自己的 heap,記錄哪些區塊已使用、哪些區塊可以重用,並處理切割、合併與對齊。
char *p = malloc(1024 * 1024);
if (!p)
return 1;
/* 此時不代表每個 page 都已經有實體 RAM */
p[0] = 'A';
free(p);
如果 allocator 手上已有足夠大的閒置區塊,malloc() 可能完全不需要進入 kernel。它只更新使用者空間的 metadata,然後回傳一段可用的虛擬位址。只有現有空間不足時,allocator 才需要向 kernel 擴充可管理的位址範圍。
因此,malloc() 回傳成功首先表示 allocator 承諾這段位址可供程式使用,不代表相同大小的實體記憶體已立即被占用。
傳統 Unix heap 可藉由 brk() 調整 program break,也就是 data segment 的尾端。提高 program break,會讓行程的 heap 虛擬位址範圍向上擴張。
較大的配置通常會使用匿名 mmap(),建立獨立的 virtual memory area(VMA)。實際門檻與策略由 allocator 實作、版本與執行狀態決定,不能把「小配置一定走 brk()、大配置一定走 mmap()」當成固定規則。
兩種方式的共同點是:kernel 先替行程建立或擴充虛擬位址映射。這個步驟主要修改 VMA 等管理資料,通常不必立刻為整段範圍配置所有實體 page。
Linux 採用 demand paging。程式第一次讀寫新取得的 page 時,CPU 發現 page table 尚未建立有效映射,便觸發 page fault。kernel 確認該位址位於合法 VMA 後,才配置或映射實體 page、更新 page table,再讓指令重新執行。
size_t size = 256UL * 1024 * 1024;
char *p = malloc(size);
for (size_t i = 0; i < size; i += 4096)
p[i] = 1;
malloc(size) 之後,行程的虛擬位址空間可能已增加;迴圈逐頁寫入時,才會陸續產生 minor page fault,並讓 resident set size(RSS)逐步成長。這就是「保留虛擬位址」與「實際駐留 RAM」之間的時間差。
讀取尚未寫入的匿名記憶體時,kernel 還可能先映射共享的唯讀 zero page。等程式第一次寫入,才配置專屬的實體 page。這進一步延後了真正的 RAM 消耗。
Linux 可以允許行程取得的虛擬記憶體承諾總量超過當下可用 RAM 與 swap,因為許多配置不會被全部觸碰,或在生命週期中從未同時駐留。這套策略稱為 memory overcommit。
相關設定可從下列介面檢視:
cat /proc/sys/vm/overcommit_memory
cat /proc/sys/vm/overcommit_ratio
grep -E 'CommitLimit|Committed_AS' /proc/meminfo
overcommit_memory 的常見模式如下:
0:使用 kernel 的 heuristic 判斷是否接受配置。1:傾向允許 overcommit。2:依 commit limit 採取較嚴格的檢查。所以 malloc() 成功不保證未來每次寫入都能取得實體記憶體。在允許 overcommit 的系統上,多個行程可能先取得很大的虛擬位址承諾,之後同時大量觸碰 page,最終造成嚴重的記憶體壓力,甚至觸發 OOM killer。
calloc() 與「已清零」的錯覺calloc() 保證回傳的記憶體內容為零,但不表示函式必須立即逐 byte 寫入整段空間。匿名 page 在交給使用者空間前本來就必須避免洩漏舊資料,而 demand paging 與 zero page 能讓清零語意延後實現。
allocator 若取得全新的匿名映射,可能知道其內容在邏輯上已為零,因此不必先在使用者空間執行一次完整的 memset()。不過,如果它重用自己管理的舊區塊,仍需要確保回傳前內容符合 calloc() 的零值保證。
free() 也不等於 RAM 立刻歸零free() 將區塊歸還給 allocator,不一定馬上歸還 kernel。allocator 常保留閒置區塊,以便後續 malloc() 快速重用。這時應用程式已經不能再存取該指標,但行程的 RSS 或虛擬位址範圍未必立即下降。
對獨立的 mmap() 區域,allocator 可能藉由 munmap() 解除映射;對 heap 內部區塊,則可能繼續留在 arena。allocator 也能用 madvise() 告訴 kernel 某些 page 的內容可被丟棄,但具體行為取決於實作與配置形狀。
因此,觀察到程式 free() 大量物件後 RSS 沒有立刻下降,不必然代表 memory leak。此時應確認這些空間是否能被 allocator 重用,以及工作集是否持續無界成長。
可以用 strace 檢視 allocator 何時真的向 kernel 調整映射:
strace -e brk,mmap,munmap,madvise ./a.out
再搭配 /proc 比較配置前、malloc() 後與逐頁寫入後的差異:
grep -E 'VmSize|VmRSS' /proc/$PID/status
cat /proc/$PID/statm
通常會看到 VmSize 先因虛擬位址映射增加,而 VmRSS 在實際觸碰 page 後才明顯上升。不過 allocator 既有 arena、Transparent Huge Pages 與取樣時機都可能影響數字,不應期待每次都恰好以 4 KiB 線性增加。
malloc() 管的是使用者空間配置語意,brk() 與 mmap() 管的是行程的虛擬位址映射,而 page fault 才常是匿名實體 page 真正進場的時刻。overcommit 允許系統先接受大於現有資源的承諾,因此配置成功也不等於未來一定不會面臨 OOM。
反過來,free() 只保證物件生命週期結束,不保證 allocator 立刻把 page 還給 kernel。理解這幾層的分工,才能正確解讀應用程式的虛擬記憶體、RSS 與實際 RAM 使用量。
明天將進一步比較 RSS、VSZ 與 PSS,看看不同工具顯示的「記憶體用量」究竟各自在計算什麼。